package com.zhima.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zhima.constant.ContestRedisConstant;
import com.zhima.constant.QuestionRedisConstant;
import com.zhima.constant.UserRedisConstant;
import com.zhima.dto.*;
import com.zhima.entity.OjJudgeRecord;
import com.zhima.enums.ContestType;
import com.zhima.enums.JudgeState;
import com.zhima.enums.QuestionScope;
import com.zhima.exception.ResourceNotAllowException;
import com.zhima.exception.ZhimaOjException;
import com.zhima.feignclient.ContestFeignClient;
import com.zhima.feignclient.QuestionFeignClient;
import com.zhima.feignclient.UserFeignClient;
import com.zhima.mapper.OjJudgeRecordMapper;
import com.zhima.service.OjCodeLanguageService;
import com.zhima.service.OjJudgeRecordService;
import com.zhima.util.PageUtil;
import com.zhima.vo.JudgeRecordVO;
import com.zhima.vo.ScoreInfo;
import com.zhima.vo.SimpleJudgeRecordVO;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import static com.zhima.constant.ContestRedisConstant.*;
import static com.zhima.constant.JudgeMqConstant.EXCHANGE_NAME;
import static com.zhima.constant.JudgeMqConstant.SUBMIT_QUEUE_BINDING_KEY;
import static com.zhima.constant.QuestionBankRedisConstant.QUESTION_BANK_USER_PROGRESS;
import static com.zhima.constant.QuestionBankRedisConstant.QUESTION_REFER;
import static com.zhima.enums.JudgeState.JUDGING;
import static com.zhima.exception.ErrorCode.ONLY_ALLOW_UPLOADER_QUERY;
import static com.zhima.exception.ErrorCode.SUBMIT_JUDGE_FAILURE;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author zhima
 * @since 2023-09-05
 */
@Service
public class OjJudgeRecordServiceImpl extends ServiceImpl<OjJudgeRecordMapper, OjJudgeRecord> implements OjJudgeRecordService {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private QuestionFeignClient questionFeignClient;
    @Autowired
    private UserFeignClient userFeignClient;
    @Autowired
    private OjCodeLanguageService ojCodeLanguageService;
    @Autowired
    private ContestFeignClient contestFeignClient;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public JudgeRecordVO queryJudgeRecordById(Long userId, Long judgeRecordId) {
        OjJudgeRecord ojJudgeRecord = lambdaQuery().eq(OjJudgeRecord::getId, judgeRecordId).one();
        FeignQuestionBasicInfoDTO feignQuestionBasicInfoDTO = questionFeignClient.queryQuestionBasicInfo(ojJudgeRecord.getQuestionId());
        // 题目范围是私有
        // 可见范围：出题人
        // 如果题目是私有的，并且查看记录的不是出题人，直接抛出异常
        if (feignQuestionBasicInfoDTO.getScope() == QuestionScope.PRIVATE && !userId.equals(feignQuestionBasicInfoDTO.getUploaderId())) {
            throw new ZhimaOjException(ONLY_ALLOW_UPLOADER_QUERY);
        }
        boolean isValid = true;
        if (feignQuestionBasicInfoDTO.getScope() == QuestionScope.PUBLIC) {
            // 题目范围是公开
            // 可见范围：出题人、本条记录的用户、AC这道题的其他用户、组赛人
            isValid =
                    // 出题人
                    userId.equals(feignQuestionBasicInfoDTO.getUploaderId())
                            || userId.equals(ojJudgeRecord.getUserId())
                            || stringRedisTemplate.opsForSet()
                            .members(QuestionRedisConstant.QUESTION_AC_PREFIX + ojJudgeRecord.getQuestionId())
                            .stream().map(Long::parseLong).collect(Collectors.toSet()).contains(userId);
            ;
        } else if (feignQuestionBasicInfoDTO.getScope() == QuestionScope.CONTEST) {
            // 题目范围是比赛
            // 可见范围：出题人、本条记录的用户、组赛人
            isValid = userId.equals(feignQuestionBasicInfoDTO.getUploaderId())
                    || userId.equals(ojJudgeRecord.getUserId());
        }
        // 是比赛提交，则判断是不是组赛人查看比赛中的提交记录
        if (ojJudgeRecord.getContestId() != null) {
            FeignSimpleContestInfo simpleContestInfo = contestFeignClient.getSimpleContestInfo(ojJudgeRecord.getContestId());
            // 比赛已经删除了，记录没有删除
            if (simpleContestInfo == null) {
                throw new ResourceNotAllowException();
            }
            // 不是组赛人也不是提交者自身
            if (!simpleContestInfo.getHolderId().equals(userId) && !userId.equals(ojJudgeRecord.getUserId())) {
                isValid = false;
            }
        }
        if (!isValid) {
            throw new ResourceNotAllowException();
        }
        return assembleJudgeRecordVO(ojJudgeRecord, userId);
    }

    @Override
    public JudgeRecordVO queryJudgeRecordById(Long judgeRecordId) {
        OjJudgeRecord ojJudgeRecord = lambdaQuery().eq(OjJudgeRecord::getId, judgeRecordId).one();
        FeignQuestionBasicInfoDTO feignQuestionBasicInfoDTO = questionFeignClient.queryQuestionBasicInfo(ojJudgeRecord.getQuestionId());
        return assembleJudgeRecordVO(ojJudgeRecord, feignQuestionBasicInfoDTO.getUploaderId());
    }

    @Override
    public String createJudgeRecord(UserDTO userDTO, SubmitJudgeDTO submitJudgeDTO) {
        OjJudgeRecord ojJudgeRecord = BeanUtil.copyProperties(submitJudgeDTO, OjJudgeRecord.class);
        // 单位 B
        FeignQuestionBasicInfoDTO feignQuestionBasicInfoDTO = questionFeignClient.queryQuestionBasicInfo(submitJudgeDTO.getQuestionId());
        ojJudgeRecord.setCodeSize(new BigDecimal(submitJudgeDTO.getCodeSizeBytes()));
        ojJudgeRecord.setUserId(userDTO.getId());
        ojJudgeRecord.setState(JudgeState.WAITING);
        ojJudgeRecord.setUserName(userDTO.getUserName());
        ojJudgeRecord.setQuestionNumber(String.format("%s%s", feignQuestionBasicInfoDTO.getPrefix(), feignQuestionBasicInfoDTO.getNumber()));
        boolean save = save(ojJudgeRecord);
        if (!save) {
            throw new ZhimaOjException(SUBMIT_JUDGE_FAILURE);
        }
        // 增加题目提交次数
        questionFeignClient.increaseSubmitNum(submitJudgeDTO.getQuestionId());
        // 增加用户提交次数
        stringRedisTemplate.opsForValue().increment(UserRedisConstant.USER_SUBMIT + userDTO.getId());
        JudgeSubmissionMessageDTO judgeSubmissionMessageDTO = new JudgeSubmissionMessageDTO();
        judgeSubmissionMessageDTO.setSubmissionRecordId(ojJudgeRecord.getId());
        judgeSubmissionMessageDTO.setVersion(questionFeignClient.queryQuestionVersion(ojJudgeRecord.getQuestionId()));
        judgeSubmissionMessageDTO.setQuestionId(submitJudgeDTO.getQuestionId());
        // TODO 可能存在多并发
        rabbitTemplate.convertAndSend(EXCHANGE_NAME, SUBMIT_QUEUE_BINDING_KEY, judgeSubmissionMessageDTO);
        // 如果是比赛中提交的，需要记录用户提交
        if (submitJudgeDTO.getContestId() != null) {
            contestFeignClient.recordUserSubmit(submitJudgeDTO.getContestId(), submitJudgeDTO.getQuestionIndex(), userDTO.getId());
        }
        return ojJudgeRecord.getId().toString();
    }

    @Override
    public List<FeignCheckJudgeRecordDTO> feignQueryJudgeRecordByShardIndex(Integer shardIndex, Integer shardSize, Integer limit) {
        // 只需要处于JUDGING状态的评测记录
        return lambdaQuery().eq(OjJudgeRecord::getState, JUDGING)
                .apply("id % {0} = {1} limit {2}", shardSize, shardIndex, limit)
                .list().stream()
                // 映射一下
                .map(r -> BeanUtil.copyProperties(r, FeignCheckJudgeRecordDTO.class))
                .collect(Collectors.toList());
    }

    @Override
    public boolean updateJudge0Result(Long judgeRecordId, FeignJudgeResult feignJudgeResult) {
        boolean updateJudgeResult = lambdaUpdate()
                .eq(OjJudgeRecord::getId, judgeRecordId)
                .set(OjJudgeRecord::getJudgeTime, new Date())
                .set(OjJudgeRecord::getScoreStrategy, feignJudgeResult.getScoreStrategy())
                .set(OjJudgeRecord::getUseTime, feignJudgeResult.getUseTime())
                .set(OjJudgeRecord::getUseMemory, feignJudgeResult.getUseMemory())
                .set(OjJudgeRecord::getScore, feignJudgeResult.getScore())
                .set(OjJudgeRecord::getState, feignJudgeResult.isAccepted() ? JudgeState.ACCEPTED : JudgeState.UNACCEPTED)
                .set(OjJudgeRecord::getTestCaseResults, JSONUtil.toJsonStr(feignJudgeResult.getSubTaskResults()))
                .update();
        OjJudgeRecord judgeRecord = getById(judgeRecordId);
        Long contestId = judgeRecord.getContestId();
        Long userId = judgeRecord.getUserId();
        String userIdStr = userId.toString();
        if (feignJudgeResult.isAccepted()) {
            // 记录本题AC的人
            stringRedisTemplate.opsForSet().add(QuestionRedisConstant.QUESTION_AC_PREFIX + judgeRecord.getQuestionId(), String.valueOf(userId));
            // 增加用户总AC次数
            stringRedisTemplate.opsForZSet().incrementScore(UserRedisConstant.USER_AC_NUM_BOARD, userId.toString(), 1);
            // 看看当前用户是否首次AC
            String questionUserAcKey = QuestionRedisConstant.QUESTION_USER_AC_PREFIX +
                    userId + ":" +
                    questionFeignClient.queryQuestionBasicInfo(judgeRecord.getQuestionId()).getDifficulty().getDifficulty();
            // 是否首次AC
            // 用户AC的题目集合里面不存在本次AC的题目id
            boolean hasAcQuestion = BooleanUtil.isFalse(stringRedisTemplate.opsForSet().isMember(questionUserAcKey, String.valueOf(judgeRecord.getQuestionId())));
            // 首次AC本题
            if (hasAcQuestion) {
                // 记录用户AC了本题
                stringRedisTemplate.opsForSet().add(questionUserAcKey, String.valueOf(judgeRecord.getQuestionId()));
                // 不重复过题数自增1
                stringRedisTemplate.opsForZSet().incrementScore(UserRedisConstant.USER_AC_BOARD, userIdStr, 1);
            }
            // 增加本题AC次数，并且更新AC率
            questionFeignClient.increaseAcNum(judgeRecord.getQuestionId());
            Set<String> questionBankIds = stringRedisTemplate.opsForSet().members(QUESTION_REFER + judgeRecord.getQuestionId());
            if (questionBankIds != null && !questionBankIds.isEmpty()) {
                for (String questionBankId : questionBankIds) {
                    // 增加过题数
                    stringRedisTemplate.opsForSet().add(String.format(QUESTION_BANK_USER_PROGRESS, questionBankId, userIdStr), judgeRecord.getQuestionId().toString());
                }
            }
        }
        if (contestId == null) {
            return updateJudgeResult;
        }
        // 更新排行榜信息
        // 得到比赛简略信息
        FeignSimpleContestInfo simpleContestInfo = contestFeignClient.getSimpleContestInfo(contestId);
        while (BooleanUtil.isFalse(stringRedisTemplate.opsForValue().setIfAbsent(String.format(CONTEST_SCORE_LOCK_PREFIX, contestId), "lock", CONTEST_SCORE_LOCK_TIME_OUT, CONTEST_SCORE_LOCK_TIME_UNIT))) {
        }
        // 用户的个人分数更新
        List<ScoreInfo.SingleQuestionScore> personalSingleQuestionScoreInfos = JSONUtil.toList((String) stringRedisTemplate.opsForHash().get(ContestRedisConstant.CONTEST_SCORE_PREFIX + contestId, userIdStr), ScoreInfo.SingleQuestionScore.class);
        // 得到对应题目的得分信息
        ScoreInfo.SingleQuestionScore needUpdateSingleQuestionScore = personalSingleQuestionScoreInfos.get(judgeRecord.getQuestionIndex());
        // 已经AC过了，不能重复加分
        if (needUpdateSingleQuestionScore.isAc()) {
            stringRedisTemplate.delete(String.format(CONTEST_SCORE_LOCK_PREFIX, contestId));
            return updateJudgeResult;
        }
        int delta = feignJudgeResult.getScore();
        // 如果是ACM方式的比赛，比赛得分就是过题数
        if (simpleContestInfo.getType() == ContestType.ACM) {
            delta = 1;
            // TODO 更新计分规则
        } else {
            // 通过比赛id和题目索引得到这道题的倍率
            delta *= contestFeignClient.getMagnification(contestId, judgeRecord.getQuestionIndex());
        }
        // 比赛排名信息更新
        stringRedisTemplate.opsForZSet().incrementScore(ContestRedisConstant.CONTEST_SCOREBOARD_PREFIX + contestId, JSONUtil.toJsonStr(new UserSimpleInfo(userId, judgeRecord.getUserName())), delta);
        needUpdateSingleQuestionScore.setAc(feignJudgeResult.isAccepted());
        // ACM赛制就是过题数
        // OI赛制就是总分
        needUpdateSingleQuestionScore.setScore(delta);
        // 本比赛本题是否已经被首次AC
        char[] acCharArray = stringRedisTemplate.opsForValue().get(ContestRedisConstant.CONTEST_AC_PREFIX + contestId).toCharArray();
        boolean isAc = acCharArray[judgeRecord.getQuestionIndex()] == '1';
        // 如果本题本场比赛尚未被AC，并且本次提交AC了
        if (!isAc && feignJudgeResult.isAccepted()) {
            acCharArray[judgeRecord.getQuestionIndex()] = '1';
            // 记录题目已经被首次AC了
            stringRedisTemplate.opsForValue().set(ContestRedisConstant.CONTEST_AC_PREFIX + contestId, String.valueOf(acCharArray));
            // 记录本比赛本题第一个AC的人
            // 比赛的首次AC榜ContestShowVO#firstAcMap
            stringRedisTemplate.opsForHash().put(ContestRedisConstant.CONTEST_FIRST_AC_USER_PREFIX + contestId, judgeRecord.getQuestionId().toString(), userIdStr);
            needUpdateSingleQuestionScore.setFirst(true);
        }
        // 更新本人本题的得分信息
        stringRedisTemplate.opsForHash().put(ContestRedisConstant.CONTEST_SCORE_PREFIX + contestId, userIdStr, JSONUtil.toJsonStr(personalSingleQuestionScoreInfos));
        stringRedisTemplate.delete(String.format(CONTEST_SCORE_LOCK_PREFIX, contestId));
        return updateJudgeResult;
    }

    @Override
    public boolean deleteQuestionJudgeRecord(Long questionId) {
        // 将所有与questionId关联的测评记录都删除了
        return lambdaUpdate().eq(OjJudgeRecord::getQuestionId, questionId).remove();
    }

    @Override
    public boolean updateAiGuide(AiGuide aiGuide) {
        return lambdaUpdate()
                .eq(OjJudgeRecord::getId, aiGuide.getJudgeRecordId())
                .set(OjJudgeRecord::getAiGuide, aiGuide.getAiGuide())
                .update();
    }

    private LambdaQueryWrapper<OjJudgeRecord> buildQueryWrapper(QueryJudgeRecordDTO queryJudgeRecordDTO) {
        if (queryJudgeRecordDTO == null) {
            return null;
        }
        LambdaQueryWrapper<OjJudgeRecord> lambdaQueryWrapper = new LambdaQueryWrapper<OjJudgeRecord>()
                .eq(queryJudgeRecordDTO.getQuestionNumber() != null && StrUtil.isNotBlank(queryJudgeRecordDTO.getQuestionNumber()), OjJudgeRecord::getQuestionNumber, queryJudgeRecordDTO.getQuestionNumber())
                .eq(queryJudgeRecordDTO.getUserId() != null, OjJudgeRecord::getUserId, queryJudgeRecordDTO.getUserId())
                .eq(queryJudgeRecordDTO.getJudgeState() != null, OjJudgeRecord::getState, queryJudgeRecordDTO.getJudgeState())
                .eq(queryJudgeRecordDTO.getCodeLanguageId() != null, OjJudgeRecord::getCodeLanguageId, queryJudgeRecordDTO.getCodeLanguageId())
                .eq(queryJudgeRecordDTO.getUserId() != null, OjJudgeRecord::getUserId, queryJudgeRecordDTO.getUserId())
                .eq(StrUtil.isNotBlank(queryJudgeRecordDTO.getContestId()), OjJudgeRecord::getContestId, queryJudgeRecordDTO.getContestId())
                // 用户名使用模糊查询
                .like(queryJudgeRecordDTO.getUserName() != null, OjJudgeRecord::getUserName, queryJudgeRecordDTO.getJudgeState());
        if (queryJudgeRecordDTO.getSortType() != null) {
            queryJudgeRecordDTO.getSortType().addCondition(lambdaQueryWrapper);
        }
        return lambdaQueryWrapper;
    }

    @Override
    public Page<SimpleJudgeRecordVO> pageQueryJudgeRecordBySomeConditions(QueryJudgeRecordDTO queryJudgeRecordDTO, Long pageNum, Long pageSize) {
        Page<OjJudgeRecord> pageQueryResult = page(new Page<>(pageNum, pageSize), buildQueryWrapper(queryJudgeRecordDTO));
        return PageUtil.buildPage(pageQueryResult, assembleOjJudgeRecord2SimpleJudgeRecordVO(pageQueryResult));
    }

    private List<SimpleJudgeRecordVO> assembleOjJudgeRecord2SimpleJudgeRecordVO(Page<OjJudgeRecord> pageQueryResult) {
        List<SimpleJudgeRecordVO> ans = new ArrayList<>();
        List<OjJudgeRecord> ojJudgeRecords = pageQueryResult.getRecords();
        for (OjJudgeRecord ojJudgeRecord : ojJudgeRecords) {
            SimpleJudgeRecordVO simpleJudgeRecordVO = BeanUtil.copyProperties(ojJudgeRecord, SimpleJudgeRecordVO.class);
            UserDTO userDTO = userFeignClient.basicInfoById(ojJudgeRecord.getUserId());
            assembleUserInfo(ojJudgeRecord, simpleJudgeRecordVO, userDTO);
            ans.add(simpleJudgeRecordVO);
        }
        return ans;
    }

    private void assembleUserInfo(OjJudgeRecord ojJudgeRecord, SimpleJudgeRecordVO simpleJudgeRecordVO, UserDTO userDTO) {
        simpleJudgeRecordVO.setUserName(userDTO.getUserName());
        simpleJudgeRecordVO.setAvatarUri(userDTO.getAvatarUri());
        simpleJudgeRecordVO.setCodeLanguageName(ojCodeLanguageService.queryCodeLanguageNameById(ojJudgeRecord.getCodeLanguageId()));
        simpleJudgeRecordVO.setJudgeState(ojJudgeRecord.getState());
        FeignQuestionBasicInfoDTO feignQuestionBasicInfoDTO = questionFeignClient.queryQuestionBasicInfo(ojJudgeRecord.getQuestionId());
        simpleJudgeRecordVO.setQuestionName(String.format("%s%s %s", feignQuestionBasicInfoDTO.getPrefix(), feignQuestionBasicInfoDTO.getNumber(), feignQuestionBasicInfoDTO.getTitle()));
    }

    private JudgeRecordVO assembleJudgeRecordVO(OjJudgeRecord ojJudgeRecord, Long userId) {
        JudgeRecordVO judgeRecordVO = new JudgeRecordVO();
        BeanUtil.copyProperties(ojJudgeRecord, judgeRecordVO);
        judgeRecordVO.setCodeLanguageName(ojCodeLanguageService.queryCodeLanguageNameById(judgeRecordVO.getCodeLanguageId()));
        // 填充userName、avatarUri
        UserDTO userDTO = userFeignClient.basicInfoById(userId);
        assembleUserInfo(ojJudgeRecord, judgeRecordVO, userDTO);
        return judgeRecordVO;
    }

    @Override
    public boolean updateJudgeRecordState(Long judgeRecordId, JudgeState judgeRecordState) {
        return lambdaUpdate().eq(OjJudgeRecord::getId, judgeRecordId).set(OjJudgeRecord::getJudgeTime, LocalDateTime.now()).set(OjJudgeRecord::getState, judgeRecordState).update();
    }

}
